﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Microsoft.WindowsAzure;
using Microsoft.WindowsAzure.StorageClient;
using System.Diagnostics;
using System.IO;
using System.Threading;
using System.Text.RegularExpressions;

namespace PlatformWorkerRole
{
    public class BlobServiceAPI
    {
        public CloudStorageAccount account;
        public CloudBlobClient blobClient;

        //info: Since the FTP protocol doesn't provide functions like CopyFile, CutFile, CopyDirectory, CutDirectory
        //I decided to exclude these from the interfaces mostly because the directory or prefix mechanism provided by azure is totally crap.
        //If there's any bug, please let me know.

        //param: configSettingName, name of the connection string in PlatformWorkerRole's config
        public BlobServiceAPI(string configSettingName)
        {
            account = CloudStorageAccount.FromConfigurationSetting(configSettingName);
            blobClient = account.CreateCloudBlobClient();
            blobClient.RetryPolicy = RetryPolicies.Retry(4, TimeSpan.Zero);
        }

        public bool ListContainers(out List<CloudBlobContainer> containerList)
        {
            containerList = new List<CloudBlobContainer>();
            try
            {
                IEnumerable<CloudBlobContainer> containers = blobClient.ListContainers();
                if (null != containers && containers.Count<CloudBlobContainer>() > 0)
                {
                    containerList.AddRange(containers);
                }
                return true;
            }
            catch (StorageClientException ex)
            {
                Trace.WriteLine(ex.Message);
                return false;
            }
        }

        //param: containerName, should be all lowercase, not null or "".
        public bool CreateContainer(string containerName)
        {
            try
            {
                CloudBlobContainer container = blobClient.GetContainerReference(containerName.ToLower());
                container.CreateIfNotExist();
                return true;
            }
            catch (StorageClientException ex)
            {
                Trace.WriteLine(ex.Message);
                return false;
            }
        }

        //caution: after invocation, a container with the same name cannot be re-created for an indeterminate period of time
        public bool DeleteContainer(string containerName)
        {
            try
            {
                CloudBlobContainer container = blobClient.GetContainerReference(containerName);
                container.Delete();
                return true;
            }
            catch (StorageClientException ex)
            {
                Trace.WriteLine(ex.Message);
                return false;
            }
        }

        //info: if the container doesn't exist, it'll cause an exception which would be caught and sometime error messages would be printed, you may ignore them.
        //better be called before you create or delete a container.
        public bool IsContainerExist(string containerName)
        {
            try
            {
                CloudBlobContainer container = blobClient.GetContainerReference(containerName);
                container.FetchAttributes();
                return true;
            }
            catch (StorageClientException ex)
            {
                Trace.WriteLine(ex.Message);
                //if (ex.ErrorCode == StorageErrorCode.ResourceNotFound)
                    return false;
                //else
                 //   throw;
            }
        }

        //info: get you the reference of a container, you may not need it.
        public bool GetContainer(string containerName, out CloudBlobContainer container)
        {
            container = null;
            try
            {
                container = blobClient.GetContainerReference(containerName);
                return true;
            }
            catch (StorageClientException ex)
            {
                Trace.WriteLine(ex.Message);
                return false;
            }
        }

        //info: to set whether the container can be accessed in browser, which doesn't really matter in the ftp project
        //better set it false
        public bool SetContainerPermission(string containerName, bool isPublic)
        {
            try
            {
                CloudBlobContainer container = blobClient.GetContainerReference(containerName);
                BlobContainerPermissions permissions = new BlobContainerPermissions();
                if (isPublic)
                    permissions.PublicAccess = BlobContainerPublicAccessType.Container;
                else
                    permissions.PublicAccess = BlobContainerPublicAccessType.Off;

                container.SetPermissions(permissions);
                return true;
            }
            catch (StorageClientException ex)
            {
                Trace.WriteLine(ex.Message);
                return false;
            }
        }

        //info: list all files and directories directly under the container folder
        public bool ListBlobs(string containerName, out List<IListBlobItem> blobList)
        {
            blobList = new List<IListBlobItem>();

            try
            {
                CloudBlobContainer container = blobClient.GetContainerReference(containerName);
                IEnumerable<IListBlobItem> blobs = container.ListBlobs();
                if (null != blobs && blobs.Count<IListBlobItem>() > 0)
                {
                    foreach (IListBlobItem blob in blobs)
                        blobList.Add(blob);
                }
                return true;
            }
            catch (StorageClientException ex)
            {
                Trace.WriteLine(ex.Message);
                return false;
            }
        }

        //info: same as IsContainerExist()
        public bool IsBlobExist(string containerName, string blobName)
        {
            try
            {
                CloudBlobContainer container = blobClient.GetContainerReference(containerName);
                CloudBlob blob = container.GetBlobReference(blobName);
                blob.FetchAttributes();

                return true;
            }
            catch (StorageClientException ex)
            {
                if (ex.ErrorCode == StorageErrorCode.ResourceNotFound)
                    return false;
                else
                    throw;
            }
        }

        //info: to upload a text file less than 64MB.
        //The uri of a blob is something like http://host:port/account/container/..../filename.postfix
        //param: blobName, the full name of a blob, which is "..../filename.postfix" in the above situation.
        //The following "blobName" parameters all mean the full name of a blob.
        public bool UploadText(string containerName, string blobName, string content)
        {
            try
            {
                CloudBlobContainer container = blobClient.GetContainerReference(containerName);
                CloudBlob blob = container.GetBlobReference(blobName);
                blob.UploadText(content);
                
                return true;
            }
            catch (StorageClientException ex)
            {
                Trace.WriteLine(ex.Message);
                return false;
            }
        }

        //info: to upload a file less than 64MB with byte array.
        public bool UploadBlob(string containerName, string blobName, byte[] content)
        {
            try
            {
                CloudBlobContainer container = blobClient.GetContainerReference(containerName);
                CloudBlob blob = container.GetBlobReference(blobName);
                blob.UploadByteArray(content);

                return true;
            }
            catch (StorageClientException ex)
            {
                Trace.WriteLine(ex.Message);
                return false;
            }
        }

        //info: to upload a file less than 64MB with stream.
        public bool UploadBlob(string containerName, string blobName, Stream source)
        {
            try
            {
                CloudBlobContainer container = blobClient.GetContainerReference(containerName);
                CloudBlob blob = container.GetBlobReference(blobName);
                blob.UploadFromStream(source);

                return true;
            }
            catch (StorageClientException ex)
            {
                Trace.WriteLine(ex.Message);
                return false;
            }
        }

        //info: to upload a single block less than 64MB which is part of a file.
        //param: blobName, the name of the whole file
        //       index, imply the block is the index-th part of the whole file
        //out:   blockId, generated id for the uploaded block, which is for merging the blocks into the original file
        public bool UploadBlock(string containerName, string blobName, Stream source, int index, out string blockId)
        {
            blockId = null;
            try
            {
                CloudBlobContainer container = blobClient.GetContainerReference(containerName);
                CloudBlockBlob blob = container.GetBlockBlobReference(blobName);
                blockId = Convert.ToBase64String(System.BitConverter.GetBytes(index));
                blob.PutBlock(blockId, source, null);

                return true;
            }
            catch (StorageClientException ex)
            {
                Trace.WriteLine(ex.Message);
                return false;
            }
        }

        //info: to get all blocks of a certain file at the server side
        //You may not maintain the id list of blocks while continuously calling UploadBlock() and get the whole id list by the filename.
        public bool GetBlockList(string containerName, string blobName, out string[] blockIdList)
        {
            blockIdList = null;
            try
            {
                CloudBlobContainer container = blobClient.GetContainerReference(containerName);
                CloudBlockBlob blob = container.GetBlockBlobReference(blobName);
                IEnumerable<ListBlockItem> blocks = blob.DownloadBlockList();
                blockIdList = new string[blocks.Count<ListBlockItem>()];
                int i = 0;
                foreach (ListBlockItem block in blocks)
                {
                    blockIdList[i++] = block.Name;
                }
                return true;
            }
            catch (StorageClientException ex)
            {
                Trace.WriteLine(ex.Message);
                return false;
            }
        }

        public bool MergeBlobFromBlocks(string containerName, string blobName, string[] blockIdList)
        {
            try
            {
                CloudBlobContainer container = blobClient.GetContainerReference(containerName);
                CloudBlockBlob blob = container.GetBlockBlobReference(blobName);
                blob.PutBlockList(blockIdList);
                return true;
            }
            catch (StorageClientException ex)
            {
                Trace.WriteLine(ex.Message);
                return false;
            }
        }

        //info: same as GetContainer()
        public bool GetBlob(string containerName, string blobName, out CloudBlob blob)
        {
            blob = null;
            try
            {
                CloudBlobContainer container = blobClient.GetContainerReference(containerName);
                blob = container.GetBlobReference(blobName);
                return true;
            }
            catch (StorageClientException ex)
            {
                Trace.WriteLine(ex.Message);
                return false;
            }
        }

        public bool DownloadText(string containerName, string blobName, out string content)
        {
            content = null;
            try
            {
                CloudBlobContainer container = blobClient.GetContainerReference(containerName);
                CloudBlob blob = container.GetBlobReference(blobName);
                content = blob.DownloadText();
                return true;
            }
            catch (StorageClientException ex)
            {
                Trace.WriteLine(ex.Message);
                return false;
            }
        }

        public bool DownloadBlob(string containerName, string blobName, out byte[] content)
        {
            content = null;
            try
            {
                CloudBlobContainer container = blobClient.GetContainerReference(containerName);
                CloudBlob blob = container.GetBlobReference(blobName);
                content = blob.DownloadByteArray();
                return true;
            }
            catch (StorageClientException ex)
            {
                Trace.WriteLine(ex.Message);
                return false;
            }
        }

        public bool DownloadBlob(string containerName, string blobName, out Stream destination)
        {
            destination = new MemoryStream();
            try
            {
                CloudBlobContainer container = blobClient.GetContainerReference(containerName);
                CloudBlob blob = container.GetBlobReference(blobName);
                blob.DownloadToStream(destination);
                return true;
            }
            catch (StorageClientException ex)
            {
                Trace.WriteLine(ex.Message);
                return false;
            }
        }

        //info: delete a file.
        //If you use this function to delete a directory without any content, the result can be right.
        //However, it's highly recommended to use DeleteDirectory() to delete a directory, whether it has any content.
        public bool DeleteBlob(string containerName, string blobName)
        {
            try
            {
                CloudBlobContainer container = blobClient.GetContainerReference(containerName);
                CloudBlob blob = container.GetBlobReference(blobName);
                blob.DeleteIfExists();
                return true;
            }
            catch (StorageClientException ex)
            {
                Trace.WriteLine(ex.Message);
                return false;
            }
        }
        /*
        public bool CopyBlob(string srcContainerName, string srcBlobName, string destContainerName, string destBlobName)
        {
            try
            {
                CloudBlobContainer srcContainer = blobClient.GetContainerReference(srcContainerName);
                CloudBlob srcBlob = srcContainer.GetBlobReference(srcBlobName);
                CloudBlobContainer destContainer = blobClient.GetContainerReference(destContainerName);
                CloudBlob destBlob = destContainer.GetBlobReference(destBlobName);
                destBlob.CopyFromBlob(srcBlob);

                return true;
            }
            catch (StorageClientException ex)
            {
                Trace.WriteLine(ex.Message);
                return false;
            }
        }

        public bool CutBlob(string srcContainerName, string srcBlobName, string destContainerName, string destBlobName)
        {
            try
            {
                CloudBlobContainer srcContainer = blobClient.GetContainerReference(srcContainerName);
                CloudBlob srcBlob = srcContainer.GetBlobReference(srcBlobName);
                CloudBlobContainer destContainer = blobClient.GetContainerReference(destContainerName);
                CloudBlob destBlob = destContainer.GetBlobReference(destBlobName);
                destBlob.CopyFromBlob(srcBlob);
                srcBlob.DeleteIfExists();
                return true;
            }
            catch (StorageClientException ex)
            {
                Trace.WriteLine(ex.Message);
                return false;
            }
        }
        
        //info: Assume the cloud storage is like:
        // |-container
        //   |-dir1
        //You can simply call CreateDirectory() once with the param "dir1/dir2/dir3/" and the storage will be like:
        // |-contaienr
        //   |-dir1
        //     |-dir2
        //       |-dir3
        //which means you don't need to create "dir1/dir2/" before create "dir1/dir2/dir3/".
        public bool CreateDirectory(string containerName, string dirName)
        {
            try
            {
                CloudBlobContainer container = blobClient.GetContainerReference(containerName);
                string directoryName = FormalizeDirectoryName(dirName);
                CloudBlob blob = container.GetBlobReference(directoryName);
                blob.UploadText("");
                return true;
            }
            catch (StorageClientException ex)
            {
                Trace.WriteLine(ex.Message);
                return false;
            }
        }

        //info: the function will delete the directory along with the content recursively.
        public bool DeleteDirectory(string containerName, string dirName)
        {
            try
            {
                List<IListBlobItem> blobList;
                ListDirectory(containerName, dirName, out blobList);
                if (null != blobList)
                {
                    foreach (IListBlobItem blob in blobList)
                    {
                        if (blob is CloudBlob)
                        {
                            ((CloudBlob)blob).DeleteIfExists();
                        }
                        else
                        {
                            string blobUri = blob.Uri.ToString();
                            int index = blobUri.IndexOf(containerName);
                            if (index > 0)
                            {
                                string subDirName = blobUri.Substring(index + containerName.Length + 1);
                                DeleteDirectory(containerName, subDirName);
                            }
                            else
                            {
                                throw new Exception("Wrong directory name.");
                            }
                        }
                    }
                }

                string directoryName = FormalizeDirectoryName(dirName);
                
                CloudBlobContainer container = blobClient.GetContainerReference(containerName);
                CloudBlob dir = container.GetBlobReference(directoryName);
                dir.DeleteIfExists();
                return true;
            }
            catch (StorageClientException ex)
            {
                Trace.WriteLine(ex.Message);
                return false;
            }
        }

        //info: list all the content of a directory.
        //IListBlobItem has two extended classes: CloudBlob and CloudBlobDirectory, which can be used to distinguish whether an item is a file or a directory.
        public bool ListDirectory(string containerName, string dirName, out List<IListBlobItem> fileList)
        {
            fileList = null;
            try
            { 
                CloudBlobContainer container = blobClient.GetContainerReference(containerName);
                //IEnumerable<IListBlobItem> blobList = container.ListBlobs();
                //var query = from item in blobList where IsContent(item.Uri, containerName, dirName) select item;
                
                string directoryName = FormalizeDirectoryName(dirName);
                CloudBlobDirectory dir = container.GetDirectoryReference(dirName);
                IEnumerable<IListBlobItem> blobs = dir.ListBlobs();
                /*
                IEnumerable<IListBlobItem> blobs = blobClient.ListBlobsWithPrefix(containerName + "/" + directoryName);
                if (null != blobs && blobs.Count<IListBlobItem>() > 0)
                {
                    fileList = new List<IListBlobItem>();
                    foreach (IListBlobItem blob in blobs)
                    {
                        //if (!blob.Uri.Equals(dir.Uri))
                            fileList.Add(blob);
                    }
                }
                
                Trace.WriteLine(String.Format("The blobs has {0} items.", blobs.Count()));
                if (null != blobs)
                {
                    fileList = new List<IListBlobItem>();
                    foreach (IListBlobItem blob in blobs)
                    {
                        if (IsContent(blob.Uri, containerName, dirName))
                            fileList.Add(blob);
                    }
                }
                return true;
                
            }
            catch (StorageClientException ex)
            {
                Trace.WriteLine(ex.Message);
                return false;
            }
        }
        */

        //info: At present, the directory is implemented by the specific file "*.directory" since the directory support by azure is crap.
        //That means if you want to create a directory named "test", you'll get a "test.directory" file uploaded to cloud.
        //However, the content of the directory would not change, which means a subfile named "test/file.txt" would not be "test.directory/file.txt".
        public bool CreateDirectory(string containerName, string dirName)
        {
            try
            {
                CloudBlobContainer container = blobClient.GetContainerReference(containerName);
                string dirFileName = ConstructDirectoryName(dirName);
                CloudBlob blob = container.GetBlobReference(dirFileName);
                blob.UploadText("");
                return true;
            }
            catch (StorageClientException ex)
            {
                Trace.WriteLine(ex.Message);
                return false;
            }
        }

        //info: you can only use this function to delete an empty directory, which is exactly what the FTP protocol means.
        //Better call IsDirectory() before calling this function.
        public bool DeleteDirectory(string containerName, string dirName)
        {
            try
            {
                List<IListBlobItem> blobs;
                ListDirectory(containerName, dirName, out blobs);
                if (null == blobs || blobs.Count == 0)
                {
                    CloudBlobContainer container = blobClient.GetContainerReference(containerName);
                    string dirFileName = ConstructDirectoryName(dirName);
                    CloudBlob blob = container.GetBlobReference(dirFileName);
                    blob.DeleteIfExists();
                    return true;
                }
                else
                    return false;
            }
            catch (StorageClientException ex)
            {
                Trace.WriteLine(ex.Message);
                return false;
            }
        }

        //info: same as IsBlobExist()
        public bool IsDirectoryExist(string containerName, string dirName)
        {
            try
            {
                CloudBlobContainer container = blobClient.GetContainerReference(containerName);
                string dirFileName = ConstructDirectoryName(dirName);
                return IsBlobExist(containerName, dirFileName);
            }
            catch (StorageClientException ex)
            {
                Trace.WriteLine(ex.Message);
                return false;
            }
        }

        //info: you'll get a list after calling this function.
        //The list contains only CloudBlob objects, which is an extended class of IListBlobItem.
        //You can distinguish whether the object is a file or a directory by its name, that is, an object with its name like "*.directory" would be a sub directory of the given directory.
        public bool ListDirectory(string containerName, string dirName, out List<IListBlobItem> fileList)
        {
            fileList = null;
            try
            {
                if (null == dirName)
                    return false;

                CloudBlobContainer container = blobClient.GetContainerReference(containerName);
                IEnumerable<IListBlobItem> blobs = blobClient.ListBlobsWithPrefix(containerName + "/" + FormalizeDirectoryName(dirName));

                if (null != blobs)
                {
                    Trace.WriteLine(String.Format("The blobs has {0} items.", blobs.Count()));
                    fileList = new List<IListBlobItem>();
                    foreach (IListBlobItem blob in blobs)
                    {
                        if (IsContent(blob.Uri, containerName, dirName))
                            fileList.Add(blob);
                    }
                    return true;
                }
                else
                    return false;
            }
            catch (StorageClientException ex)
            {
                Trace.WriteLine(ex.Message);
                return false;
            }
        }

        /*
        public bool CopyDirectory(string srcContainerName, string srcDirName, string destContainerName, string destDirName)
        {
            string srcDirectoryName = FormalizeDirectoryName(srcDirName);
            string destDirectoryName = FormalizeDirectoryName(destDirName);
            try
            {
                List<IListBlobItem> blobList;
                ListDirectory(srcContainerName, srcDirectoryName, out blobList);
                if (null != blobList)
                {
                    foreach (IListBlobItem blob in blobList)
                    {
                        if (blob is CloudBlob)
                        {
                            string originalBlobName = ((CloudBlob)blob).Name;
                            CopyBlob(srcContainerName, originalBlobName, destContainerName, ConstructNewBlobName(originalBlobName, srcDirectoryName, destDirectoryName));
                        }
                        else
                        {
                            string originalDirName = BlobUriToName(blob.Uri, srcContainerName);
                            CopyDirectory(srcContainerName, originalDirName, destContainerName, ConstructNewBlobName(originalDirName, srcDirectoryName, destDirectoryName));
                        }
                    }
                }
                
                return true;
            }
            catch (StorageClientException ex)
            {
                Trace.WriteLine(ex.Message);
                return false;
            }
        }

        public bool CutDirectory(string srcContainerName, string srcDirName, string destContainerName, string destDirName)
        {
            bool result = true;
            result = result && CopyDirectory(srcContainerName, srcDirName, destContainerName, destDirName);
            result = result && DeleteDirectory(srcContainerName, srcDirName);
            return result;
        }

        public void TestDirectory()
        {
            try
            {
                CloudBlobContainer container = blobClient.GetContainerReference("testcontainer");

                CloudBlob blob = container.GetBlobReference("dir/sub/blob.txt");
                blob.UploadText("Test blob.");

                Trace.WriteLine(blob.Parent.Uri);

                blob = container.GetBlobReference("dir/sub2/");
                blob.UploadText("");

                Trace.WriteLine(blob.Uri);

                //CopyBlob("testcontainer", "");

                CloudBlobDirectory dir = container.GetDirectoryReference("dir/sub");
                if (null == dir)
                    Trace.WriteLine("No such directory exists.");
                else
                {
                    IEnumerable<IListBlobItem> blobs = dir.ListBlobs();
                    foreach (IListBlobItem item in blobs)
                    {
                        Trace.WriteLine(item.GetType().Name + " " + item.Uri);
                        if (item is CloudBlobDirectory && !item.Uri.Equals(dir.Uri))
                        {
                            IEnumerable<IListBlobItem> subs = ((CloudBlobDirectory)item).ListBlobs();
                            foreach (IListBlobItem sub in subs)
                                Trace.WriteLine("\t" + sub.GetType().Name + " " + sub.Uri);
                        }
                    }
                }
            }
            catch (StorageClientException ex)
            {
                Trace.WriteLine(ex.Message);
            }
        }
        */
        //info: check whether the input parameter ends with "/", concatenate it with "/" if not.
        //If you don't need it, you may change it to private.
        public string FormalizeDirectoryName(string originalName)
        {
            if (string.IsNullOrEmpty(originalName))
                return originalName;

            string dirName = originalName;
            if (!dirName.EndsWith("/"))
                dirName += "/";

            return dirName;
        }

        //info: construct the specific file name "[originalName].directory"
        //If the originalName is "dir" or "dir/", the result should both be "dir.directory"
        public string ConstructDirectoryName(string originalName)
        {
            if (string.IsNullOrEmpty(originalName))
                return originalName;

            string dirName = originalName;
            if (dirName.EndsWith("/"))
                dirName = dirName.Substring(0, dirName.Length - 1);
            dirName += ".directory";
            return dirName;
        }

        //info: it's for constructing the name of a blob under a certain directory and it's used in the recursion of CopyDirectory() and CutDirectory().
        //If you don'n need it, you may change it to private.
        public string ConstructNewBlobName(string originalBlobName, string srcDirName, string destDirName)
        {
            if (string.IsNullOrEmpty(originalBlobName) || null == srcDirName || null == destDirName)
                return originalBlobName;
            string srcDirectoryName = FormalizeDirectoryName(srcDirName);
            string destDirectoryName = FormalizeDirectoryName(destDirName);
            int index = originalBlobName.IndexOf(srcDirectoryName);
            StringBuilder builder = new StringBuilder();
            builder.Append(destDirectoryName);
            builder.Append(originalBlobName.Substring(index + srcDirectoryName.Length));
            return builder.ToString();
        }

        //info: extract the name of a blob from its uri.
        //If you don't need it, you may change it to private.
        public string BlobUriToName(Uri blobUri, string containerName)
        {
            if (null == blobUri)
                return null;

            string blobName = blobUri.ToString();
            int index = blobName.IndexOf(containerName);
            blobName = blobName.Substring(index + containerName.Length);
            return blobName;
        }

        //info: check whether a file is content of a directory
        public bool IsContent(Uri blobUri, string containerName, string dirName)
        {
            string blobName = BlobUriToName(blobUri, containerName);
            StringBuilder pattern = new StringBuilder();
            pattern.Append(FormalizeDirectoryName(dirName));
            pattern.Append("[a-zA-Z0-9]*\\.[a-zA-Z0-9]*");
            return Regex.IsMatch(blobName, pattern.ToString());
        }
    }
}
